Support cargo-driven wasm-bindgen linking - #27179
Conversation
15227f6 to
7017b20
Compare
| # section so emcc, when used as the linker (e.g. by cargo/rustc), knows to run | ||
| # wasm-bindgen as a post-link step. | ||
| with webassembly.Module(wasm_file) as module: | ||
| return module.get_custom_section('__wasm_bindgen_emscripten_marker') is not None |
There was a problem hiding this comment.
Can we just have the rust compiler pass a flag rather than having magic behaviour like this? Maybe -sWASM_BINDGEN=auto (or some better name)?
There was a problem hiding this comment.
I'm open to a flag. For rustc users it would be possible to pass as a -Clink-arg=-s... then. -sWASM_BINDGEN currently means a C/C++ build with Rust being linked in. We'd need to think more about if we could fully unify with that path, since right now it's a different type of bindgen path. Maybe it can be unified I'm not sure.
-sWASM_BINDGEN_RUST or something that makes it clear Rust is in charge of the top-level entry point bindings might make sense though.
Not sure what the best term is here either!
7017b20 to
099c7c5
Compare
When cargo/rustc drives the build with emcc as the linker, the linked wasm carries a __wasm_bindgen_emscripten_marker custom section and rustc supplies the exact -sEXPORTED_FUNCTIONS. Detect that marker in phase_post_link and run wasm-bindgen as a post-link step, the same way the -sWASM_BINDGEN staticlib flow does, without any export discovery. This defines two clearly-separated modes: - C++-driven (-sWASM_BINDGEN set): the user owns EXPORTED_FUNCTIONS; their exports are left untouched. - marker-driven (set not passed, marker detected): rustc's EXPORTED_FUNCTIONS is the raw wasm export set the generated glue reaches by name (the method shims, the __wbindgen_* runtime, the marker, main), not a user-facing API. The user-facing API is exactly what wasm-bindgen self-registers via its library (wasm-bindgen 0.2.126), so the rustc-supplied set is dropped from every user-export layer: the ESM wrapper (WASM_ESM_INTEGRATION, via user_requested_exports) and the factory Module attachment (MODULARIZE, via EXPORTED_FUNCTIONS / should_export), and the keepalive wasm exports are not surfaced. main still runs automatically on init, matching the emscripten idiom, even though _main is not exported. Both output modes then expose only the clean API (e.g. a `Greeter` class). - Strip the placeholder symbols wasm-bindgen consumes (__wbindgen_describe*, __externref_*, ...) from EXPORTED_FUNCTIONS so they aren't reported as undefined exports. - Wire imported JS: feed library_bindgen.extern-pre.js as extern-pre-js and copy the snippets/ dir next to the output so relative imports resolve. - The WASM_ESM_INTEGRATION wrapper re-exports the JS library symbols that were exported (MODULARIZE=instance), derived from the JS compiler's librarySymbols and EXPORTED_FUNCTIONS. - Under WASM_ESM_INTEGRATION, provide wasmExports via a namespace import of the wasm so by-name export access (e.g. wasm-bindgen's glue) works. Add an end-to-end test (test/rust/bindgen_greeter) parameterized over the ESM and factory output modes, and install a pinned wasm-bindgen-cli alongside rust in CI so the flow is always exercised.
099c7c5 to
bcade0d
Compare
|
I was able to actually address properly unifying the linking model in #27208, which then means we don't need to distinguish who the driver is. Then |
…ction (#27208) This unifies wasm-bindgen output generation under a single `-sWASM_BINDGEN` path, building on the wasm-bindgen side change in wasm-bindgen/wasm-bindgen#5210. It supersedes #27179. Previously `-sWASM_BINDGEN` was effectively a staticlib-only flow where emcc had to discover and own the export set itself. With wasm-bindgen/wasm-bindgen#5210, wasm-bindgen hoists its clean exported API (functions, classes, enums) into top-level library symbols that self-register as public exports via the `__export`/`__force` symbol attributes (#27436, wasm-bindgen/wasm-bindgen#5253) in its `--js-library`. The user-facing API now comes from wasm-bindgen's own library in every flow, so emscripten no longer needs to guess or own exports — it just surfaces what wasm-bindgen registered. That lets one `-sWASM_BINDGEN` path serve both whether cargo/rustc drives the link (bin crate, emcc as the linker, rustc supplies `-sEXPORTED_FUNCTIONS`) or emcc drives it (staticlib, exports discovered locally). The model for a public export is the union across both systems: wasm-bindgen's self-registered API, plus emscripten's own `EMSCRIPTEN_KEEPALIVE` exports — minus wasm-bindgen's internal expansion glue, which must not spill into the public surface. What's implemented: * `-sWASM_BINDGEN` is a no-op unless the linker inputs carry wasm-bindgen's `__wasm_bindgen_emscripten_marker` custom section (emitted by the wasm-bindgen crate). The check runs on the inputs before the link (`llvm-objdump --section-headers`, so archive members are covered), so there is a single link and no speculative exports. When the marker is absent wasm-bindgen is never invoked and need not be installed, so cargo/rustc can pass the flag unconditionally via `-Clink-arg` without affecting ordinary builds. * The exports the wasm-bindgen expansion reaches by name — the supplied `EXPORTED_FUNCTIONS` (method shims, the `__wbindgen_*` runtime, the marker, `main`) plus anything its expansion adds — are captured as internal glue and kept off every export layer: the ESM wrapper (`user_requested_exports`), the factory `Module` attachment (`EXPORTED_FUNCTIONS`, via `should_export`), and the keepalive pass in `finalize_wasm`. `main` still runs automatically on init; `_main` isn't surfaced. * A genuine `EMSCRIPTEN_KEEPALIVE` C/C++ export is **not** in that internal set and remains surfaced, so a hand-written native export composes with wasm-bindgen's API in the same module. * Placeholder symbols wasm-bindgen consumes (`__wbindgen_describe*`, `__externref_*`, ...) are stripped so they aren't reported as undefined exports. * nm-based export discovery only runs when no driver supplied `EXPORTED_FUNCTIONS`; the rustc-driven link already lists them exactly. * Imported JS is wired through: `library_bindgen.extern-pre.js` is fed as extern-pre-js and the `snippets/` dir is copied next to the output so relative imports resolve. * Under `WASM_ESM_INTEGRATION`, `wasmExports` is provided via a namespace import of the wasm so wasm-bindgen's by-name export access works. This is gated behind `WASM_BINDGEN` so plain ESM integration keeps per-symbol named imports and their tree-shakability. (The wrapper re-export of self-registered JS library symbols itself landed in #27436.) Both output modes then expose only the clean API (e.g. a `Greeter` class). Testing: * End-to-end test parameterized over the ESM (`-sWASM_ESM_INTEGRATION`) and factory (`-sMODULARIZE -sEXPORT_ES6`) output modes, built via `cargo build` with `-sWASM_BINDGEN` passed as a link arg, asserting the clean `Greeter` API works, `main` runs on init, and none of the raw wasm exports leak. * A no-marker test asserting `-sWASM_BINDGEN` is a no-op for an ordinary `hello_world.c` build (doesn't require wasm-bindgen installed). * The staticlib integration test is extended to assert an `EMSCRIPTEN_KEEPALIVE` native export survives alongside the wasm-bindgen API (fails under a blanket export wipe). * CI installs a pinned `wasm-bindgen-cli` alongside rust so the flow is always exercised. Not in scope (follow-up): preserving human-supplied `EXPORTED_FUNCTIONS` through wasm-bindgen linkage (the rustc-supplied set is currently indistinguishable from glue, so explicit exports aren't surfaced).
This implements the cargo/rustc-driven wasm-bindgen flow, where cargo/rustc drives the build with emcc as the linker instead of building a staticlib and passing
-sWASM_BINDGENexplicitly.When rustc links via emcc, the linked wasm carries a
__wasm_bindgen_emscripten_markercustom section and rustc supplies the exact-sEXPORTED_FUNCTIONS. We detect that marker inphase_post_linkand run wasm-bindgen as a post-link step, the same way the-sWASM_BINDGENstaticlib flow does, with no export discovery.The key correctness insight is that wasm exports are distinct from user exports. Everything rustc lists in
EXPORTED_FUNCTIONSis a wasm export the generated glue reaches by name (the method shims, the__wbindgen_*runtime, the marker,main), not a user-facing export. With recent wasm-bindgen (0.2.126), the user-facing API is exactly what wasm-bindgen self-registers via its JS library. So we drop the rustc-supplied set from every user-export layer:WASM_ESM_INTEGRATION), viauser_requested_exportsModuleattachment (MODULARIZE), viaEXPORTED_FUNCTIONS/should_exportBoth output modes then expose only the clean API. For example a crate exporting a
Greeterclass yields:with no
__wbindgen_*, method shims, marker, ormainleaking into the user-facing surface.Other pieces of the flow:
__wbindgen_describe*,__externref_*, ...) fromEXPORTED_FUNCTIONS/USER_EXPORTSso they aren't reported as undefined exports.library_bindgen.extern-pre.jsas extern-pre-js and copy thesnippets/dir next to the output so relative imports resolve.MODULARIZE=instance) so theWASM_ESM_INTEGRATIONwrapper re-exports them.WASM_ESM_INTEGRATION && WASM_BINDGEN, providewasmExportsvia a namespace import of the wasm so the glue's by-name access works.Test coverage adds an end-to-end test (
test/rust/bindgen_greeter) parameterized over both the ESM and factory output modes, building the crate viacargo build(emcc as linker, marker auto-detected) and verifying in Node that the clean class works and that no raw wasm exports leak. CI installs a pinnedwasm-bindgen-clialongside rust (the two are always installed together) so the flow is always exercised, with arequires_wasm_bindgenskip for local runs.This PR was made with AI assistance, under my review.